"use strict";

exports.__esModule = true;
require("core-js/modules/es.error.cause.js");
require("core-js/modules/es.array.push.js");
require("core-js/modules/es.array.to-sorted.js");
require("core-js/modules/esnext.iterator.constructor.js");
require("core-js/modules/esnext.iterator.every.js");
require("core-js/modules/esnext.iterator.filter.js");
require("core-js/modules/esnext.iterator.find.js");
require("core-js/modules/esnext.iterator.map.js");
require("core-js/modules/esnext.iterator.reduce.js");
var _handsontableEditor = require("../handsontableEditor");
var _array = require("../../helpers/array");
var _object = require("../../helpers/object");
var _element = require("../../helpers/dom/element");
var _mixed = require("../../helpers/mixed");
var _string = require("../../helpers/string");
var _unicode = require("../../helpers/unicode");
var _textRenderer = require("../../renderers/textRenderer");
var _a11y = require("../../helpers/a11y");
var _function = require("../../helpers/function");
function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); }
function _classPrivateFieldInitSpec(e, t, a) { _checkPrivateRedeclaration(e, t), t.set(e, a); }
function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
function _classPrivateFieldGet(s, a) { return s.get(_assertClassBrand(s, a)); }
function _assertClassBrand(e, t, n) { if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; throw new TypeError("Private element is not present on this object"); }
const EDITOR_TYPE = exports.EDITOR_TYPE = 'autocomplete';

/**
 * @private
 * @class AutocompleteEditor
 */
var _idPrefix = /*#__PURE__*/new WeakMap();
var _focusDebounced = /*#__PURE__*/new WeakMap();
var _AutocompleteEditor_brand = /*#__PURE__*/new WeakSet();
class AutocompleteEditor extends _handsontableEditor.HandsontableEditor {
  constructor() {
    super(...arguments);
    /**
     * Fix width of the internal Handsontable's instance when editor has vertical scroll.
     */
    _classPrivateMethodInitSpec(this, _AutocompleteEditor_brand);
    /**
     * Query string to turn available values over.
     *
     * @type {string}
     */
    _defineProperty(this, "query", null);
    /**
     * Contains stripped choices.
     *
     * @type {string[]}
     */
    _defineProperty(this, "strippedChoices", []);
    /**
     * Contains raw choices.
     *
     * @type {Array}
     */
    _defineProperty(this, "rawChoices", []);
    /**
     * Holds the prefix of the editor's id.
     *
     * @type {string}
     */
    _classPrivateFieldInitSpec(this, _idPrefix, this.hot.guid.slice(0, 9));
    /**
     * Runs focus method after debounce.
     */
    _classPrivateFieldInitSpec(this, _focusDebounced, (0, _function.debounce)(() => {
      this.focus();
    }, 100));
  }
  static get EDITOR_TYPE() {
    return EDITOR_TYPE;
  }
  /**
   * Gets current value from editable element.
   *
   * @returns {string}
   */
  getValue() {
    const selectedValue = this.rawChoices.find(value => {
      const strippedValue = this.stripValueIfNeeded(value);
      return (_assertClassBrand(_AutocompleteEditor_brand, this, _isKeyValueObject).call(this, strippedValue) ? strippedValue.value : strippedValue) === this.TEXTAREA.value;
    });
    if ((0, _mixed.isDefined)(selectedValue)) {
      return selectedValue;
    }
    return this.TEXTAREA.value;
  }

  /**
   * Creates an editor's elements and adds necessary CSS classnames.
   */
  createElements() {
    super.createElements();
    (0, _element.addClass)(this.htContainer, 'autocompleteEditor');
    (0, _element.addClass)(this.htContainer, this.hot.rootWindow.navigator.platform.indexOf('Mac') === -1 ? '' : 'htMacScroll');
    if (this.hot.getSettings().ariaTags) {
      (0, _element.setAttribute)(this.TEXTAREA, [(0, _a11y.A11Y_TEXT)(), (0, _a11y.A11Y_COMBOBOX)(), (0, _a11y.A11Y_HASPOPUP)('listbox'), (0, _a11y.A11Y_AUTOCOMPLETE)()]);
    }
  }

  /**
   * Prepares editor's metadata and configuration of the internal Handsontable's instance.
   *
   * @param {number} row The visual row index.
   * @param {number} col The visual column index.
   * @param {number|string} prop The column property (passed when datasource is an array of objects).
   * @param {HTMLTableCellElement} td The rendered cell element.
   * @param {*} value The rendered value.
   * @param {object} cellProperties The cell meta object (see {@link Core#getCellMeta}).
   */
  prepare(row, col, prop, td, value, cellProperties) {
    super.prepare(row, col, prop, td, value, cellProperties);
    if (this.hot.getSettings().ariaTags) {
      (0, _element.setAttribute)(this.TEXTAREA, [(0, _a11y.A11Y_EXPANDED)('false'), (0, _a11y.A11Y_CONTROLS)(`${_classPrivateFieldGet(_idPrefix, this)}-listbox-${row}-${col}`)]);
    }
    this.htOptions = {
      ...this.htOptions,
      valueGetter: cellValue => _assertClassBrand(_AutocompleteEditor_brand, this, _isKeyValueObject).call(this, cellValue) ? cellValue.value : cellValue
    };
  }

  /**
   * Opens the editor and adjust its size and internal Handsontable's instance.
   */
  open() {
    super.open();
    const trimDropdown = this.cellProperties.trimDropdown === undefined ? true : this.cellProperties.trimDropdown;
    const rootInstanceAriaTagsEnabled = this.hot.getSettings().ariaTags;
    const sourceArray = Array.isArray(this.cellProperties.source) ? this.cellProperties.source : null;
    const sourceSize = sourceArray === null || sourceArray === void 0 ? void 0 : sourceArray.length;
    const {
      row: rowIndex,
      col: colIndex
    } = this;
    this.showEditableElement();
    this.focus();
    this.addHook('beforeKeyDown', event => this.onBeforeKeyDown(event));
    this.htEditor.addHook('afterScroll', _classPrivateFieldGet(_focusDebounced, this));
    this.htEditor.updateSettings({
      colWidths: trimDropdown ? [(0, _element.outerWidth)(this.TEXTAREA) - 2] : undefined,
      autoColumnSize: true,
      renderer: (hotInstance, TD, row, col, prop, value, cellProperties) => {
        (0, _textRenderer.textRenderer)(hotInstance, TD, row, col, prop, value, cellProperties);
        const {
          filteringCaseSensitive,
          allowHtml,
          locale
        } = this.cellProperties;
        const query = this.query;
        let cellValue = (0, _mixed.stringify)(value);
        let indexOfMatch;
        let match;
        if (cellValue && !allowHtml) {
          indexOfMatch = filteringCaseSensitive === true ? cellValue.indexOf(query) : cellValue.toLocaleLowerCase(locale).indexOf(query.toLocaleLowerCase(locale));
          if (indexOfMatch !== -1) {
            match = cellValue.substr(indexOfMatch, query.length);
            cellValue = cellValue.replace(match, `<strong>${match}</strong>`);
          }
        }
        if (rootInstanceAriaTagsEnabled) {
          (0, _element.setAttribute)(TD, [(0, _a11y.A11Y_OPTION)(),
          // Add `setsize` and `posinset` only if the source is an array.
          ...(sourceArray ? [(0, _a11y.A11Y_SETSIZE)(sourceSize)] : []), ...(sourceArray ? [(0, _a11y.A11Y_POSINSET)(sourceArray.indexOf(value) + 1)] : []), ['id', `${this.htEditor.rootElement.id}_${row}-${col}`]]);
        }
        TD.innerHTML = cellValue;
      },
      afterSelectionEnd: (startRow, startCol) => {
        if (rootInstanceAriaTagsEnabled) {
          const setA11yAttributes = TD => {
            (0, _element.setAttribute)(TD, [(0, _a11y.A11Y_SELECTED)()]);
            (0, _element.setAttribute)(this.TEXTAREA, ...(0, _a11y.A11Y_ACTIVEDESCENDANT)(TD.id));
          };
          const TD = this.htEditor.getCell(startRow, startCol, true);
          if (TD !== null) {
            setA11yAttributes(TD);
          } else {
            // If TD is null, it means that the cell is not (yet) in the viewport.
            // Moving the logic to after it's been scrolled to the requested cell.
            this.htEditor.addHookOnce('afterScrollVertically', () => {
              const renderedTD = this.htEditor.getCell(startRow, startCol, true);
              setA11yAttributes(renderedTD);
            });
          }
        }
      }
    });
    if (rootInstanceAriaTagsEnabled) {
      // Add `role=presentation` to the main table to prevent the readers from treating the option list as a table.
      (0, _element.setAttribute)(this.htEditor.view._wt.wtOverlays.wtTable.TABLE, ...(0, _a11y.A11Y_PRESENTATION)());
      (0, _element.setAttribute)(this.htEditor.rootElement, [(0, _a11y.A11Y_LISTBOX)(), (0, _a11y.A11Y_LIVE)('polite'), (0, _a11y.A11Y_RELEVANT)('text'), ['id', `${_classPrivateFieldGet(_idPrefix, this)}-listbox-${rowIndex}-${colIndex}`]]);
      (0, _element.setAttribute)(this.TEXTAREA, ...(0, _a11y.A11Y_EXPANDED)('true'));
    }
    this.hot._registerTimeout(() => {
      this.queryChoices(this.TEXTAREA.value);
    });
  }

  /**
   * Closes the editor.
   */
  close() {
    this.removeHooksByKey('beforeKeyDown');
    super.close();
    if (this.hot.getSettings().ariaTags) {
      (0, _element.setAttribute)(this.TEXTAREA, [(0, _a11y.A11Y_EXPANDED)('false')]);
    }
  }

  /**
   * Verifies result of validation or closes editor if user's cancelled changes.
   *
   * @param {boolean|undefined} result If `false` and the cell using allowInvalid option,
   *                                   then an editor won't be closed until validation is passed.
   */
  discardEditor(result) {
    super.discardEditor(result);
    this.hot.view.render();
  }

  /**
   * Prepares choices list based on applied argument.
   *
   * @param {string} query The query.
   */
  queryChoices(query) {
    const source = this.cellProperties.source;
    this.query = query;
    if (typeof source === 'function') {
      source.call(this.cellProperties, query, choices => {
        this.rawChoices = choices;
        this.updateChoicesList(this.stripValuesIfNeeded(choices));
      });
    } else if (Array.isArray(source)) {
      this.rawChoices = source;
      this.updateChoicesList(this.stripValuesIfNeeded(source));
    } else {
      this.updateChoicesList([]);
    }
  }

  /**
   * Updates list of the possible completions to choose.
   *
   * @private
   * @param {Array} choicesList The choices list to process.
   */
  updateChoicesList(choicesList) {
    const pos = (0, _element.getCaretPosition)(this.TEXTAREA);
    const endPos = (0, _element.getSelectionEndPosition)(this.TEXTAREA);
    const sortByRelevanceSetting = this.cellProperties.sortByRelevance;
    const filterSetting = this.cellProperties.filter;
    const value = this.stripValueIfNeeded(this.getValue());
    const comparableValue = _assertClassBrand(_AutocompleteEditor_brand, this, _isKeyValueObject).call(this, value) ? value.value : value;
    let highlightIndex = null;
    let choices = choicesList;
    if (!sortByRelevanceSetting) {
      choices = choices.toSorted();
    }
    const filteredChoiceIndexes = [];
    const locale = this.cellProperties.locale;
    const filteringCaseSensitive = this.cellProperties.filteringCaseSensitive;
    const valueToMatch = filteringCaseSensitive ? comparableValue : comparableValue.toLocaleLowerCase(locale);
    for (let i = 0; i < choices.length; i++) {
      const currentItem = _assertClassBrand(_AutocompleteEditor_brand, this, _isKeyValueObject).call(this, choices[i]) ? (0, _string.stripTags)((0, _mixed.stringify)(choices[i].value)) : (0, _string.stripTags)((0, _mixed.stringify)(choices[i]));
      const itemToMatch = filteringCaseSensitive ? currentItem : currentItem.toLocaleLowerCase(locale);
      if (itemToMatch.indexOf(valueToMatch) !== -1) {
        filteredChoiceIndexes.push(i);
        if (filterSetting === false) {
          break;
        }
      }
    }
    if (filterSetting === false) {
      highlightIndex = filteredChoiceIndexes[0];
    } else {
      choices = filteredChoiceIndexes.map(index => choices[index]);
      highlightIndex = choices.indexOf(valueToMatch) > -1 ? choices.indexOf(valueToMatch) : 0;
    }
    this.strippedChoices = choices;
    if (choices.length === 0) {
      this.htEditor.rootElement.style.display = 'none';
    } else {
      this.htEditor.rootElement.style.display = '';
    }
    this.htEditor.loadData((0, _array.pivot)([choices]));
    if (choices.length > 0) {
      this.updateDropdownDimensions();
      this.flipDropdownVerticallyIfNeeded();
      if (this.cellProperties.strict === true) {
        this.highlightBestMatchingChoice(highlightIndex);
      }
    }
    this.hot.listen();
    (0, _element.setCaretPosition)(this.TEXTAREA, pos, pos === endPos ? undefined : endPos);
  }

  /**
   * Calculates the space above and below the editor and flips it vertically if needed.
   *
   * @private
   * @returns {{ isFlipped: boolean, spaceAbove: number, spaceBelow: number}}
   */
  flipDropdownVerticallyIfNeeded() {
    const result = super.flipDropdownVerticallyIfNeeded();
    const {
      isFlipped,
      spaceAbove,
      spaceBelow
    } = result;
    this.limitDropdownIfNeeded(isFlipped ? spaceAbove : spaceBelow);
    return result;
  }

  /**
   * Checks if the internal table should generate scrollbar or could be rendered without it.
   *
   * @private
   * @param {number} spaceAvailable The free space as height defined in px available for dropdown list.
   */
  limitDropdownIfNeeded(spaceAvailable) {
    const dropdownHeight = this.getDropdownHeight();
    if (dropdownHeight > spaceAvailable) {
      let tempHeight = 0;
      let lastRowHeight = 0;
      let height = null;
      do {
        lastRowHeight = this.htEditor.stylesHandler.getDefaultRowHeight();
        tempHeight += lastRowHeight;
      } while (tempHeight < spaceAvailable);
      height = tempHeight - lastRowHeight;
      if (this.isFlippedVertically) {
        this.htEditor.rootElement.style.top = `${parseInt(this.htEditor.rootElement.style.top, 10) + dropdownHeight - height}px`;
      }
      this.setDropdownHeight(tempHeight - lastRowHeight);
    }
  }

  /**
   * Updates width and height of the internal Handsontable's instance.
   *
   * @private
   */
  updateDropdownDimensions() {
    const fractionalScalingCompensation = (0, _element.getFractionalScalingCompensation)();
    const targetWidth = this.getTargetEditorWidth() + fractionalScalingCompensation;
    const targetHeight = this.getTargetEditorHeight() + fractionalScalingCompensation;
    this.htEditor.updateSettings({
      width: targetWidth,
      height: targetHeight
    });
    _assertClassBrand(_AutocompleteEditor_brand, this, _fixDropdownWidth).call(this);
    this.htEditor.view._wt.wtTable.alignOverlaysWithTrimmingContainer();
  }

  /**
   * Sets new height of the internal Handsontable's instance.
   *
   * @private
   * @param {number} height The new dropdown height.
   */
  setDropdownHeight(height) {
    this.htEditor.updateSettings({
      height
    });
    _assertClassBrand(_AutocompleteEditor_brand, this, _fixDropdownWidth).call(this);
    this.htEditor.view._wt.wtTable.alignOverlaysWithTrimmingContainer();
  }

  /**
   * Creates new selection on specified row index, or deselects selected cells.
   *
   * @private
   * @param {number|undefined} index The visual row index.
   */
  highlightBestMatchingChoice(index) {
    if (typeof index === 'number') {
      this.htEditor.selectCell(index, 0, undefined, undefined, undefined, false);
    } else {
      this.htEditor.deselectCell();
    }
  }

  /**
   * Calculates the proposed/target editor height that should be set once the editor is opened.
   * The method may be overwritten in the child class to provide a custom size logic.
   *
   * @returns {number}
   */
  getTargetEditorHeight() {
    let borderCompensation = 0;
    if (!this.hot.getCurrentThemeName()) {
      const containerStyle = this.hot.rootWindow.getComputedStyle(this.htContainer.querySelector('.htCore'));
      borderCompensation = parseInt(containerStyle.borderTopWidth, 10) + parseInt(containerStyle.borderBottomWidth, 10);
    }
    const maxItems = Math.min(this.cellProperties.visibleRows, this.strippedChoices.length);
    const height = Array.from({
      length: maxItems
    }, (_, i) => i).reduce((totalHeight, index) => {
      // for the first row, we need to add 1px (border-top compensation)
      const rowHeight = this.hot.stylesHandler.getDefaultRowHeight() + (index === 0 ? 1 : 0);
      return totalHeight + rowHeight;
    }, 0);
    return height + borderCompensation;
  }

  /**
   * Calculates the proposed/target editor width that should be set once the editor is opened.
   * The method may be overwritten in the child class to provide a custom size logic.
   *
   * @returns {number}
   */
  getTargetEditorWidth() {
    let borderCompensation = 0;
    if (!this.hot.getCurrentThemeName()) {
      const containerStyle = this.hot.rootWindow.getComputedStyle(this.htContainer.querySelector('.htCore'));
      borderCompensation = parseInt(containerStyle.borderInlineStartWidth, 10) + parseInt(containerStyle.borderInlineEndWidth, 10);
    }
    return this.htEditor.getColWidth(0) + borderCompensation;
  }

  /**
   * Sanitizes value from potential dangerous tags.
   *
   * @private
   * @param {string} value The value to sanitize.
   * @returns {string}
   */
  stripValueIfNeeded(value) {
    return this.stripValuesIfNeeded([value])[0];
  }

  /**
   * Sanitizes an array of the values from potential dangerous tags.
   *
   * @private
   * @param {string[]} values The value to sanitize.
   * @returns {Array<string|{key: string, value: string}>}
   */
  stripValuesIfNeeded(values) {
    const {
      allowHtml
    } = this.cellProperties;
    const processValue = value => (0, _mixed.stringify)(allowHtml ? value : (0, _string.stripTags)(value));
    if (values.every(value => _assertClassBrand(_AutocompleteEditor_brand, this, _isKeyValueObject).call(this, value))) {
      return values.map(value => {
        return {
          key: processValue(value.key),
          value: processValue(value.value)
        };
      });
    }
    return values.map(value => processValue(value));
  }
  /**
   * OnBeforeKeyDown callback.
   *
   * @private
   * @param {KeyboardEvent} event The keyboard event object.
   */
  onBeforeKeyDown(event) {
    if ((0, _unicode.isPrintableChar)(event.keyCode) || event.keyCode === _unicode.KEY_CODES.BACKSPACE || event.keyCode === _unicode.KEY_CODES.DELETE || event.keyCode === _unicode.KEY_CODES.INSERT) {
      // for Windows 10 + FF86 there is need to add delay to make sure that the value taken from
      // the textarea is the freshest value. Otherwise the list of choices does not update correctly (see #7570).
      // On the more modern version of the FF (~ >=91) it seems that the issue is not present or it is
      // more difficult to induce.
      let timeOffset = 10;

      // on ctl+c / cmd+c don't update suggestion list
      if (event.keyCode === _unicode.KEY_CODES.C && (event.ctrlKey || event.metaKey)) {
        return;
      }
      if (!this.isOpened()) {
        timeOffset += 10;
      }
      if (this.htEditor) {
        this.hot._registerTimeout(() => {
          this.queryChoices(this.TEXTAREA.value);
        }, timeOffset);
      }
    }
  }
}
exports.AutocompleteEditor = AutocompleteEditor;
function _fixDropdownWidth() {
  if (this.htEditor.view.hasVerticalScroll()) {
    this.htEditor.updateSettings({
      width: this.getTargetEditorWidth() + (0, _element.getScrollbarWidth)(this.hot.rootDocument)
    });
  }
}
/**
 * Checks if the value is a key/value object.
 *
 * @param {*} value The value to check.
 * @returns {boolean}
 */
function _isKeyValueObject(value) {
  return (0, _object.isObject)(value) && (0, _mixed.isDefined)(value.key) && (0, _mixed.isDefined)(value.value);
}